// Copyright (c) 2014 Liyong Zou
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
// OR OTHER DEALINGS IN THE SOFTWARE.



#ifndef OBJECT_HPP
#define OBJECT_HPP

#include "from_lua.hpp"
#include "to_lua.hpp"

extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}

#include <string>
#include <typeinfo>
#include <memory>
#include <sstream>

#include <iostream>

#ifdef USE_BOOST
#include <boost/shared_ptr.hpp>
using boost::shared_ptr;
#else
using std::shared_ptr;
#endif

namespace luaglue {

class object;

class luaobject
{
    public:
    // for global
    explicit luaobject(lua_State *L)
        : m_L(L)
    {
        push_ref_table(L); // push ref table
        lua_pushglobaltable(L); // push _G
        m_type = lua_type(L, -1);
        m_ref = luaL_ref(L, -2); // pop _G
        lua_pop(L, 1); // pop ref table
    }

    // for stack object
    explicit luaobject(lua_State *L, int index)
        : m_L(L)
    {
        push_ref_table(L); // push ref table
        index = (index > 0)? index : index - 1; // the relative location of object is change when pushed the ref table 
        lua_pushvalue(L, index); // push value
        m_type = lua_type(L, -1);
        m_ref = luaL_ref(L, -2); // pop value
        lua_pop(L, 1); // pop ref table
    }

    static const shared_ptr<luaobject> global(lua_State *L)
    {
        shared_ptr<luaobject> object_global(new luaobject(L));
        return object_global;
    }

    static const shared_ptr<luaobject> nil(lua_State *L)
    {
        lua_pushnil(L); // push nil
        shared_ptr<luaobject> object_nil(new luaobject(L, -1));
        lua_pop(L, -1); // pop nil
        return object_nil;
    }

    static const shared_ptr<luaobject> new_table(lua_State *L)
    {
        shared_ptr<luaobject> newtab(new luaobject);

        newtab->push_ref_table(L); // push ref table
        lua_newtable(L); // create new table and push it
        newtab->m_type = lua_type(L, -1);
        newtab->m_ref = luaL_ref(L, -2); // pop new table
        lua_pop(L, 1); // pop ref table
        newtab->m_L = L;

        return newtab;
    }

    // for table
    template <class T>
    luaobject(const shared_ptr<luaobject> &t, const T &field)
        : m_L(t->vm())
    {
        ref(m_L, t, field);
    }

    // destructor
    ~luaobject()
    {
        unref(m_L, m_ref);
    }

    // cast
    template <class T>
    holder<T> cast(void) const
    {
        push_ref_table(m_L); //push ref table
        lua_rawgeti(m_L, -1, m_ref); // push value
        holder<T> value = from_lua<T>()(m_L, -1); // transform
        lua_pop(m_L, 2); // clear stack
        return value;
    }

    inline lua_State *vm(void) const
    {
        return m_L;
    }

    inline int type(void) const
    {
        return m_type;
    }

    inline void push(void) const
    {
        if ((LUA_NOREF == m_ref) || (LUA_REFNIL == m_ref))
        {
            lua_pushnil(m_L);
        }
        else
        {
            push_ref_table(m_L); // push ref table
            lua_rawgeti(m_L, -1, m_ref); // push ref value
            lua_remove(m_L, -2); // pop ref table
        }
    }

    private:
    luaobject()
    {
    }

    template <class T>
    void ref(lua_State *L, const shared_ptr<luaobject> &t, const T &field)
    {
        if(LUA_TTABLE == t->type())
        {
            push_ref_table(L);
            lua_rawgeti(L, -1, t->m_ref); // push parent
            to_lua(L, field); // push key
            lua_gettable(L, -2); // pop key and push ref value
            m_type = lua_type(L, -1);
            m_ref = luaL_ref(L, -3); // pop ref value
            lua_pop(L, 2);
        }
        else
        {
            m_type = LUA_TNIL;
            m_ref = LUA_NOREF;
        }
    }

    inline void unref(lua_State *L, int ref)
    {
        push_ref_table(L);
        luaL_unref(L, -1, ref);
        lua_pop(L, 1); // pop metatable
    }

    inline void push_ref_table(lua_State *L) const
    {
        luaL_newmetatable(L, typeid(this).name()); // push metatable
    }

    luaobject(const luaobject &){
    }

    lua_State *m_L;
    int m_type;
    int m_ref;
};



template <class TF>
class table_field
{
public:
inline table_field(const shared_ptr<luaobject> &t, TF field)
    : m_table(t)
    , m_field(field)
    , m_L(t->vm())
{
    // nothing to do
}



template <class TV>
inline table_field<TF>& operator=(const TV &v)
{
    m_table->push(); // push table
    to_lua(m_L, m_field); // push key
    to_lua(m_L, v); // push value
    lua_settable(m_L, -3); // pop value and key
    lua_pop(m_L, 1); // pop table

    return *this;
}



template <class T>
table_field<T> operator[](T field) const
{
    return object(*this)[field];
}

inline const shared_ptr<luaobject> &table(void) const
{
    return m_table;
}

inline const TF &field(void) const
{
    return m_field;
}

private:
    shared_ptr<luaobject>   m_table;
    TF                      m_field;
    lua_State               *m_L;
};


class object
{
public:
    explicit object(lua_State *L)
        : m_data(luaobject::nil(L))
    {

    }

    explicit object(lua_State *L, int index)
        : m_data(new luaobject(L, index))
    {
    }

    template <class T>
    explicit object(const object &t, T field)
        : m_data(new luaobject(t.m_data, field))
    {
    }

    template <class T>
    explicit object(lua_State *L, T field)
        : m_data(new luaobject(luaobject::global(L), field))
    {
    }

    template <class T>
    object(const table_field<T> &tf)
        : m_data(new luaobject(tf.table(), tf.field()))
    {

    }

    static object global(lua_State *L)
    {
        object global;
        global.m_data = luaobject::global(L);
        return global;
    }

    static object nil(lua_State *L)
    {
        object nil;
        nil.m_data = luaobject::nil(L);
        return nil;
    }

    static object new_table(lua_State *L)
    {
        object newtab;
        newtab.m_data = luaobject::new_table(L);
        return newtab;
    }

    inline lua_State *vm(void) const
    {
        return m_data->vm();
    }

    inline int type(void) const
    {
        return m_data->type();
    }

    inline void push(void) const
    {
        m_data->push();
    }

    template <class T>
    holder<T> cast(void)
    {
        return m_data->cast<T>();
    }

    template<class T>
    table_field<T> operator[](T field) const
    {
        return table_field<T>(m_data, field);
    }

private:
    object() {}
    shared_ptr<luaobject>   m_data;
};



template <>
class from_lua<object>
{
public:
    holder<object> operator()(lua_State *L, int index)
    {
        return object(L, index);
    }
};

template <>
inline void to_lua<object>(lua_State *, object o)
{
    o.push();
}

template <class T>
inline void to_lua(lua_State *, table_field<T> tf)
{
    object(tf).push();
}

}



#define foreach_in_table(table, key, value) \
    (table).push(); \
    for (lua_pushnil((table).vm()); \
            (0 != lua_next((table).vm(), -2)) && \
            (((key) = luaglue::object((table).vm(), -2)), true) && \
            (((value) = luaglue::object((table).vm(), -1)), true); \
            lua_pop((table).vm(), 1) \
        )

#endif // OBJECT_H

